package com.jandar.mvc.util;

import com.jandar.pile.common.config.JwtConfig;
import com.jandar.pile.common.dto.JwtAccount;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import static com.jandar.pile.common.CommonConstants.AUTH_TYPE;

@Component
@Slf4j
public class JwtUtil<P, R> {
    private final JwtConfig config;

    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    private static final String CLAIM_KEY_ISSUER = "iss";
    private static final String CLAIM_KEY_PRIMARY = "id";
    private static final String CLAIM_KEY_ROLE = "role";
    private static final String CLAIM_KEY_DEPT = "dept";
    private static final String CLAIM_SYSTEM_CODE = "systemCode";
    private static final String CLAIM_DEP_CODE = "depCode";
    private static final String CLAIM_RYBM = "rybm";
    private static final String CLAIM_DWBM = "dwbm";

    /***
     * 偏差
     */
    private static final int DEVIATION = 1000;

    public JwtUtil(JwtConfig config) {
        this.config = config;
    }

    public String getShowName(String token) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        return String.valueOf(claims.get("sub"));
    }
    public Integer getUserId(String token) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        return (Integer) claims.get("id");
    }

    public String getDwbm(String token) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        return (String) claims.get("dwbm");
    }
    public String getRybm(String token) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        return (String) claims.get("rybm");
    }
    public String getDepCode(String token) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        return (String) claims.get("depCode");
    }
    public String getSystemCode(String token) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        return (String) claims.get("systemCode");
    }

    public Integer getRoleId(String token) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        return (Integer) claims.get("role");
    }

    public Integer getDepId(String token) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        return (Integer) claims.get("dept");
    }

    public R getRoleFromToken(String token) {
        return getRoleFromToken(token, null);
    }

    /***
     * 从 Token 获取权限
     * @param token
     * @return
     */
    @SuppressWarnings("unchecked")
    public R getRoleFromToken(String token, Conversion<R> conversion) {
        R role;
        try {
            final Claims claims = getClaimsFromToken(token);
            if (claims.get(CLAIM_KEY_ROLE) == null) {
                return null;
            }
            if (conversion != null) {
                role = conversion.convert(claims.get(CLAIM_KEY_ROLE));
            } else {
                role = (R) claims.get(CLAIM_KEY_ROLE);
            }
        } catch (Exception e) {
            role = null;
        }
        return role;
    }

    /***
     * 从 Token 获取用户名
     * @param token
     * @return
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            final Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    public P getPrimaryFromToken(String token) {
        return getPrimaryFromToken(token, null);
    }

    /***
     * 从 Token 获取用户ID
     * @param token
     * @return
     */
    @SuppressWarnings("unchecked")
    public P getPrimaryFromToken(String token, Conversion<P> conversion) {
        P id;
        try {
            final Claims claims = getClaimsFromToken(token);
            if (claims.get(CLAIM_KEY_PRIMARY) == null) {
                return null;
            }
            if (conversion == null) {
                id = (P) claims.get(CLAIM_KEY_PRIMARY);
            } else {
                id = conversion.convert(claims.get(CLAIM_KEY_PRIMARY));
            }
        } catch (Exception e) {
            id = null;
        }
        return id;
    }

    /***
     * 从 Token 获取创建日期
     * @param token
     * @return
     */
    private Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = getClaimsFromToken(token);
            created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    /***
     * 从 Token 获取失效日期
     * @param token
     * @return
     */
    private Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

    /***
     * 获取 Claims
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(config.getSecurity())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /***
     * 生成 Token
     * @param account
     * @return
     */
    public String generateToken(JwtAccount<P, R> account) {
        Map<String, Object> claims = new HashMap<>(3);
        claims.put(CLAIM_KEY_USERNAME, account.getUsername());
        claims.put(CLAIM_KEY_PRIMARY, account.getPrimary());
        claims.put(CLAIM_KEY_ROLE, account.getRoleValue());
        claims.put(CLAIM_KEY_DEPT, account.getDepId());
        claims.put(CLAIM_KEY_CREATED, new Date());
        claims.put(CLAIM_KEY_ISSUER, config.getIssuer());
        claims.put(CLAIM_SYSTEM_CODE, account.getSystemCode());
        claims.put(CLAIM_DEP_CODE, account.getDepCode());
        claims.put(CLAIM_RYBM, account.getRybm());
        claims.put(CLAIM_DWBM,account.getDwbm());
//        claims.put("showName",account.getShowName());
        return generateToken(claims);
    }

    /***
     * 生成 Token
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512;
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(config.getSecurity());
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(signatureAlgorithm, signingKey)
                .compact();
    }

    /***
     * 生成失效日期
     * @return
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + Long.parseLong(config.getExpiration()) * 1000);
    }

    /***
     * Token 是否失效
     * @param token
     * @return
     */
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        boolean flag = (expiration != null && expiration.before(new Date()));
        if (flag) {
            log.info("Token常规失效");
        }
        return flag;
    }

    /***
     * 生成日期是否在最后修改密码日期之前(密码修改之前产生的 Token 全部失效)
     * @param created
     * @param lastPasswordReset
     * @return
     */
    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        boolean flag = (lastPasswordReset != null && created.before(lastPasswordReset));
        if (flag) {
            // 小于 1000ms 校正
            if ((Math.abs(created.getTime() - lastPasswordReset.getTime()) <= DEVIATION)) {
                flag = false;
            } else {
                log.info("Token因密码更新失效");
            }
        }
        return flag;
    }

    /***
     * 生成日期是否在最后修改角色日期之前(角色修改之前产生的 Token 全部失效)
     * @param created
     * @param lastRoleReset
     * @return
     */
    private Boolean isCreatedBeforeLastRoleReset(Date created, Date lastRoleReset) {
        boolean flag = (lastRoleReset != null && created.before(lastRoleReset));
        if (flag) {
            // 小于 1000ms 校正
            if (Math.abs(created.getTime() - lastRoleReset.getTime()) <= DEVIATION) {
                flag = false;
            } else {
                log.info("token因角色更新失效");
            }
        }
        return flag;
    }

    /***
     * 生成日期是否在最后登录日期之前(登录之前产生的 Token 全部失效)
     * @param created
     * @param lastLoginDate
     * @return
     */
    private Boolean isLoginBeforeLastLogin(Date created, Date lastLoginDate) {
        if (Boolean.parseBoolean(config.getTokenUnique())) {
            // token 产生日期与最后登录时间小于 1000ms 有效
            boolean flag = (lastLoginDate != null &&
                    Math.abs(created.getTime() - lastLoginDate.getTime()) >= DEVIATION);
            if (flag) {
                log.info("token因有新token产生失效");
            }
            return flag;
        }
        return false;
    }

    /***
     * Token 是否可以用于刷新
     * @param token
     * @param account
     * @return
     */
    public Boolean canTokenBeRefreshed(String token, JwtAccount<P, R> account) {
        final Date created = getCreatedDateFromToken(token);
        return !isCreatedBeforeLastPasswordReset(created, account.getLastPwdResetDate())
                && !isCreatedBeforeLastRoleReset(created, account.getLastRoleResetDate())
                && !isLoginBeforeLastLogin(created, account.getLastLoginDate())
                && !isTokenExpired(token);
    }

    /***
     * 刷新 Token
     * @param token
     * @return
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            final Claims claims = getClaimsFromToken(token);
            claims.put(CLAIM_KEY_CREATED, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /***
     * 验证 Token 有效性
     * @param token
     * @param account
     * @return
     */
    public Boolean validateToken(String token, JwtAccount<P, R> account) {
        final String username = getUsernameFromToken(token);
        final Date created = getCreatedDateFromToken(token);
        return (
                account.getUsername().equals(username)
                        && account.nonLocked()
                        && !isTokenExpired(token)
                        && !isCreatedBeforeLastPasswordReset(created, account.getLastPwdResetDate())
                        && !isCreatedBeforeLastRoleReset(created, account.getLastRoleResetDate())
                        && !isLoginBeforeLastLogin(created, account.getLastLoginDate()));
    }

    /***
     * 获取具体认证信息
     * @param authHeader
     * @return
     */
    public String getToken(String authHeader) {
        if (authHeader != null && authHeader.startsWith(AUTH_TYPE)) {
            return authHeader.substring(AUTH_TYPE.length()).trim();
        } else {
            return authHeader;
        }
    }


    public JwtConfig getConfig() {
        return config;
    }

    public interface Conversion<T> {
        /***
         * 自定义数据转换,举例 Long.parse(data.toString())
         * @param data
         * @return
         */
        T convert(Object data);
    }
}
